﻿Imports System.Windows.Media
Imports System.Math
Public Class TrendLine
    Inherits ChartDrawingVisual
    Implements ISelectable, ISerializable

    'Private chart As Chart
    'Public Sub New(ByVal chart As Chart)
    '    Me.chart = chart
    'End Sub
    Public Sub New()
    End Sub

    Private baseLineGeometry As New LineGeometry
    Private parallelLineGeometry As New LineGeometry

    Private _drawing As DrawingGroup
    Protected Overrides Property Drawing As System.Windows.Media.DrawingGroup
        Get
            Return _drawing
        End Get
        Set(ByVal value As System.Windows.Media.DrawingGroup)
            _drawing = value
        End Set
    End Property

    Private _relativeCoordinates As New LineCoordinates

    Private Function GetSnappedCoordinates() As LineCoordinates
        Dim coordinates As LineCoordinates
        If SnapToRelativePixels Then
            coordinates.StartPoint = New Point(Round(StartPoint.X), StartPoint.Y)
        Else
            coordinates.StartPoint = _relativeCoordinates.StartPoint
        End If
        If SnapToRelativePixels Then
            coordinates.EndPoint = New Point(Round(EndPoint.X), EndPoint.Y)
        Else
            coordinates.EndPoint = _relativeCoordinates.EndPoint
        End If
        Return coordinates
    End Function

    Public Property StartPoint As Point
        Get
            Return _relativeCoordinates.StartPoint
        End Get
        Set(ByVal value As Point)
            _relativeCoordinates.StartPoint = value
            If AutoRefresh Then RefreshVisual()
        End Set
    End Property
    Public Property EndPoint As Point
        Get
            Return _relativeCoordinates.EndPoint
        End Get
        Set(ByVal value As Point)
            _relativeCoordinates.EndPoint = value
            If AutoRefresh Then RefreshVisual()
        End Set
    End Property
    Public Property Coordinates As LineCoordinates
        Get
            Return New LineCoordinates(StartPoint, EndPoint)
        End Get
        Set(ByVal value As LineCoordinates)
            StartPoint = value.StartPoint
            EndPoint = value.EndPoint
        End Set
    End Property

    Private _isSelected As Boolean
    Public Property IsSelected As Boolean Implements ISelectable.IsSelected
        Get
            Return _isSelected
        End Get
        Set(ByVal value As Boolean)
            _isSelected = value
            RefreshVisual()
        End Set
    End Property
    Public Property IsSelectable As Boolean = True Implements ISelectable.IsSelectable
    Public Overrides Property IsEditable As Boolean = True

    Private _parallelDistance As Double
    Public Property ParallelDistance As Double
        Get
            Return _parallelDistance
        End Get
        Set(ByVal value As Double)
            _parallelDistance = value
            If AutoRefresh Then RefreshVisual()
        End Set
    End Property
    Private _hasParallel As Boolean
    Public Property HasParallel As Boolean
        Get
            Return _hasParallel
        End Get
        Set(ByVal value As Boolean)
            _hasParallel = value
            RefreshVisual()
        End Set
    End Property

    Private _pen As Pen = New Pen(Nothing, 1)
    Public Overrides Property Pen As System.Windows.Media.Pen
        Get
            Return _pen
        End Get
        Set(ByVal value As System.Windows.Media.Pen)
            _pen = value
            If AutoRefresh Then RefreshVisual()
        End Set
    End Property
    Private _extendRight As Boolean
    Public Property ExtendRight As Boolean
        Get
            Return _extendRight
        End Get
        Set(ByVal value As Boolean)
            _extendRight = value
            If AutoRefresh Then RefreshVisual()
        End Set
    End Property
    Private _extendLeft As Boolean
    Public Property ExtendLeft As Boolean
        Get
            Return _extendLeft
        End Get
        Set(ByVal value As Boolean)
            _extendLeft = value
            If AutoRefresh Then RefreshVisual()
        End Set
    End Property

    Public Property DragCircleSize As Double = 6

    Public Overrides ReadOnly Property Parent As Chart
        Get
            Return MyBase.Parent
        End Get
    End Property


    Public Overrides Sub RefreshVisual()
        If HasParent Then
            Dim realLineCoordinates As LineCoordinates
            If UseNegativeCoordinates Then
                realLineCoordinates = Parent.GetRealFromRelative(GetSnappedCoordinates)
            Else
                realLineCoordinates = Parent.GetRealFromRelative(NegateY(GetSnappedCoordinates))
            End If

            Dim realCoordinates As LineCoordinates = realLineCoordinates

            If ExtendRight Then realLineCoordinates = GetExtendedRightCoordinates(realLineCoordinates)
            If ExtendLeft Then realLineCoordinates = GetExtendedLeftCoordinates(realLineCoordinates)
            baseLineGeometry = New LineGeometry(realLineCoordinates.StartPoint, realLineCoordinates.EndPoint)

            Dim realParallelDistance As Double = Parent.GetRealFromRelativeHeight(ParallelDistance)
            Dim realParallelLineCoordinates As New LineCoordinates(New Point(baseLineGeometry.StartPoint.X, baseLineGeometry.StartPoint.Y + BooleanToInteger(UseNegativeCoordinates) * realParallelDistance), New Point(baseLineGeometry.EndPoint.X, baseLineGeometry.EndPoint.Y + BooleanToInteger(UseNegativeCoordinates) * realParallelDistance))
            If ExtendRight Then realParallelLineCoordinates = GetExtendedRightCoordinates(realParallelLineCoordinates)
            If ExtendLeft Then realParallelLineCoordinates = GetExtendedLeftCoordinates(realParallelLineCoordinates)
            If HasParallel Then
                parallelLineGeometry = New LineGeometry(realParallelLineCoordinates.StartPoint, realParallelLineCoordinates.EndPoint)
            End If
            Dim lightweightGeometry As GeometryGroup = Nothing

            If Not IsSelected Then
                lightweightGeometry = New GeometryGroup
                lightweightGeometry.Children.Add(baseLineGeometry)
                If HasParallel Then lightweightGeometry.Children.Add(parallelLineGeometry)
            Else
                _drawing = New DrawingGroup
                Dim brush As SolidColorBrush
                If Parent IsNot Nothing Then
                    brush = New SolidColorBrush(GetForegroundColor(CType(Parent.Background, SolidColorBrush).Color))
                Else
                    brush = Brushes.White
                End If
                _drawing.Children.Add(New GeometryDrawing(Pen.Brush, Pen, baseLineGeometry))
                _drawing.Children.Add(New GeometryDrawing(brush, New Pen(brush, 1), GetDotsGeometryForLine(realLineCoordinates, realCoordinates, True)))
                If HasParallel Then
                    _drawing.Children.Add(New GeometryDrawing(Pen.Brush, Pen, parallelLineGeometry))
                    _drawing.Children.Add(New GeometryDrawing(brush, New Pen(brush, 1), GetDotsGeometryForLine(realParallelLineCoordinates, New LineCoordinates(New Point(realCoordinates.StartPoint.X, realCoordinates.StartPoint.Y + BooleanToInteger(UseNegativeCoordinates) * realParallelDistance), New Point(realCoordinates.EndPoint.X, realCoordinates.EndPoint.Y + BooleanToInteger(UseNegativeCoordinates) * realParallelDistance)), False)))
                End If
            End If
            If DrawVisual Then
                If Parent.GetRelativeFromReal(realLineCoordinates).ToBounds.ToWindowsRect.IntersectsWith(Parent.Bounds.ToWindowsRect) Then
                    If IsSelected Then
                        Using dc As DrawingContext = RenderOpen()
                            dc.DrawDrawing(_drawing)
                        End Using
                    Else
                        Using dc As DrawingContext = RenderOpen()
                            dc.DrawGeometry(Pen.Brush, Pen, lightweightGeometry)
                        End Using
                    End If
                Else
                    Dim dc As DrawingContext = RenderOpen()
                    dc.Close()
                End If
            End If
        End If
    End Sub

    Private Function GetDotsGeometryForLine(ByVal coordinates As LineCoordinates, ByVal endDotPoints As LineCoordinates, ByVal showEndDots As Boolean) As GeometryGroup
        Dim group As New GeometryGroup
        Dim length As Double = Distance(coordinates.StartPoint, coordinates.EndPoint)
        Dim squareCount As Double
        Select Case length
            Case Is >= 400
                squareCount = Ceiling(length / 150) + 1
            Case Is >= 60
                squareCount = 3
            Case Is >= 20
                squareCount = 1
            Case Is < 20
                squareCount = 0
        End Select
        squareCount = 1 / (squareCount + 1)
        For t = squareCount To 1 - squareCount Step squareCount
            group.Children.Add(New EllipseGeometry(New Windows.Rect((coordinates.EndPoint.X - coordinates.StartPoint.X) * t + coordinates.StartPoint.X - DragCircleSize / 3, (coordinates.EndPoint.Y - coordinates.StartPoint.Y) * t + coordinates.StartPoint.Y - DragCircleSize / 3, DragCircleSize / 1.5, DragCircleSize / 1.5)))
        Next
        If showEndDots Then
            group.Children.Add(New EllipseGeometry(New Windows.Rect(endDotPoints.StartPoint.X - DragCircleSize / 2, endDotPoints.StartPoint.Y - DragCircleSize / 2, DragCircleSize, DragCircleSize)))
            group.Children.Add(New EllipseGeometry(New Windows.Rect(endDotPoints.EndPoint.X - DragCircleSize / 2, endDotPoints.EndPoint.Y - DragCircleSize / 2, DragCircleSize, DragCircleSize)))
        End If
        Return group
    End Function

    Private Function GetExtendedRightCoordinates(ByVal coordinates As LineCoordinates) As LineCoordinates
        With coordinates
            Dim bounds As Bounds = Parent.GetRealFromRelative(Parent.Bounds)
            Dim startPoint As Point
            Dim endPoint As Point
            Dim lineBackwards As Boolean

            If .EndPoint.X = .EndPoint.Y Then

            ElseIf .EndPoint.X > .StartPoint.X Then
                startPoint = .StartPoint
                endPoint = .EndPoint
            Else
                lineBackwards = True
                startPoint = .EndPoint
                endPoint = .StartPoint
            End If

            Dim edge As Double
            If endPoint.Y <= startPoint.Y Then
                edge = bounds.Y1
            Else
                edge = bounds.Y2
            End If
            If endPoint.X <> startPoint.X Then
                Dim xPoint As Point = New Point(bounds.X2, LinCalc(startPoint.X, startPoint.Y, endPoint.X, endPoint.Y, bounds.X2))
                Dim xDis As Double = Distance(startPoint, xPoint)
                Dim yPoint As Point = New Point(LinCalc(startPoint.Y, startPoint.X, endPoint.Y, endPoint.X, edge), edge)
                Dim yDis As Double = Distance(startPoint, yPoint)
                If xDis <= yDis Or Double.IsNaN(yDis) Then
                    endPoint = xPoint
                Else
                    endPoint = yPoint
                End If
                If lineBackwards Then
                    .StartPoint = endPoint
                Else
                    .EndPoint = endPoint
                End If
            Else
                If .EndPoint.Y < .StartPoint.Y Then
                    .EndPoint = New Point(.EndPoint.X, bounds.Y1)
                Else
                    .StartPoint = New Point(.StartPoint.X, bounds.Y1)
                End If
            End If
        End With
        Return coordinates
    End Function
    Private Function GetExtendedLeftCoordinates(ByVal coordinates As LineCoordinates) As LineCoordinates
        With coordinates
            Dim bounds As Bounds = Parent.GetRealFromRelative(Parent.Bounds)
            Dim startPoint As Point
            Dim endPoint As Point
            Dim lineBackwards As Boolean
            If .EndPoint.X <= .StartPoint.X Then
                startPoint = .StartPoint
                endPoint = .EndPoint
            Else
                lineBackwards = True
                startPoint = .EndPoint
                endPoint = .StartPoint
            End If

            Dim edge As Double
            If endPoint.Y <= startPoint.Y Then
                edge = bounds.Y1
            Else
                edge = bounds.Y2
            End If
            If endPoint.X <> startPoint.X Then
                Dim xPoint As Point = New Point(bounds.X1, LinCalc(startPoint.X, startPoint.Y, endPoint.X, endPoint.Y, bounds.X1))
                Dim xDis As Double = Distance(startPoint, xPoint)
                Dim yPoint As Point = New Point(LinCalc(startPoint.Y, startPoint.X, endPoint.Y, endPoint.X, edge), edge)
                Dim yDis As Double = Distance(startPoint, yPoint)
                If xDis <= yDis Or Double.IsNaN(yDis) Then
                    startPoint = xPoint
                Else
                    startPoint = yPoint
                End If
                If lineBackwards Then
                    .StartPoint = startPoint
                Else
                    .EndPoint = startPoint
                End If
            Else
                If .EndPoint.Y > .StartPoint.Y Then
                    .EndPoint = New Point(.EndPoint.X, bounds.Y2)
                Else
                    .StartPoint = New Point(.StartPoint.X, bounds.Y2)
                End If
            End If
        End With
        Return coordinates
    End Function


    Protected Overrides Function PopupInformation() As System.Collections.Generic.List(Of System.Collections.Generic.KeyValuePair(Of String, String))
        Dim startpoint As Point = GetSnappedCoordinates.StartPoint
        Dim endpoint As Point = GetSnappedCoordinates.EndPoint
        Dim items As New List(Of KeyValuePair(Of String, String))
        items.Add(New KeyValuePair(Of String, String)("Start Price: ", FormatNumber(If(UseNegativeCoordinates, -1, 1) * startpoint.Y, Parent.Settings("DecimalPlaces").Value).Replace(",", "")))
        items.Add(New KeyValuePair(Of String, String)("Start Bar: ", startpoint.X))
        If HasParent AndAlso startpoint.X > 0 AndAlso Parent.bars.Count >= startpoint.X Then
            items.Add(New KeyValuePair(Of String, String)("Start Date: ", Parent.bars(startpoint.X - 1).Data.Date.ToShortDateString))
            items.Add(New KeyValuePair(Of String, String)("Start Time: ", Parent.bars(startpoint.X - 1).Data.Date.ToShortTimeString))
        Else
            items.Add(New KeyValuePair(Of String, String)("Start Date: ", "N/A"))
            items.Add(New KeyValuePair(Of String, String)("Start Time: ", "N/A"))
        End If
        items.Add(New KeyValuePair(Of String, String)("End Bar: ", endpoint.X))
        items.Add(New KeyValuePair(Of String, String)("End Price: ", FormatNumber(If(UseNegativeCoordinates, -1, 1) * endpoint.Y, Parent.Settings("DecimalPlaces").Value).Replace(",", "")))
        If HasParent AndAlso endpoint.X > 0 AndAlso Parent.bars.Count >= endpoint.X Then
            items.Add(New KeyValuePair(Of String, String)("End Date: ", Parent.bars(endpoint.X - 1).Data.Date.ToShortDateString))
            items.Add(New KeyValuePair(Of String, String)("End Time: ", Parent.bars(endpoint.X - 1).Data.Date.ToShortTimeString))
        Else
            items.Add(New KeyValuePair(Of String, String)("End Date: ", "N/A"))
            items.Add(New KeyValuePair(Of String, String)("End Time: ", "N/A"))
        End If
        items.Add(New KeyValuePair(Of String, String)("Bar Spread: ", Abs(endpoint.X - startpoint.X)))
        items.Add(New KeyValuePair(Of String, String)("Price Spread: ", FormatNumber(Abs(endpoint.Y - startpoint.Y), Parent.Settings("DecimalPlaces").Value).Replace(",", "")))
        If AnalysisTechnique IsNot Nothing Then
            items.Add(New KeyValuePair(Of String, String)("Analysis Technique: ", AnalysisTechnique.Name))
        Else
            items.Add(New KeyValuePair(Of String, String)("Analysis Technique: ", "N/A"))
        End If
        Return items
    End Function

    Private dragAreaClicked As Integer = -1
    Private mouseOffset As Point
    Private clickCoordinates As LineCoordinates
    Public Overrides Sub ParentMouseLeftButtonDown(ByVal e As MouseButtonEventArgs, ByVal location As System.Windows.Point)
        MyBase.ParentMouseLeftButtonDown(e, location)

        If UseNegativeCoordinates Then
            clickCoordinates = Parent.GetRealFromRelative(_relativeCoordinates)
            mouseOffset = location - clickCoordinates.StartPoint
        Else
            clickCoordinates = Parent.GetRealFromRelative(NegateY(_relativeCoordinates))
            mouseOffset = location - Parent.GetRealFromRelative(NegateY(StartPoint))
        End If

        If Distance(location, clickCoordinates.StartPoint) < Pen.Thickness + DragCircleSize / 2 + ClickLeniency Then
            dragAreaClicked = 1
        ElseIf Distance(location, clickCoordinates.EndPoint) < Pen.Thickness + DragCircleSize / 2 + ClickLeniency Then
            dragAreaClicked = 2
            mouseOffset = location - clickCoordinates.EndPoint
        Else
            If HasParallel Then
                If baseLineGeometry.FillContains(location, ClickLeniency, ToleranceType.Absolute) Then
                    dragAreaClicked = 0
                Else
                    dragAreaClicked = 3
                End If
            Else
                dragAreaClicked = 0
            End If
        End If
        IsSelected = True
    End Sub
    Public Overrides Sub ParentMouseMove(ByVal e As MouseEventArgs, ByVal location As System.Windows.Point)
        If IsMouseDown And IsEditable Then
            Dim prevAutoRefresh As Boolean = AutoRefresh
            AutoRefresh = False

            Dim realCoordinates As LineCoordinates
            If UseNegativeCoordinates Then
                realCoordinates = Parent.GetRealFromRelative(_relativeCoordinates)
            Else
                realCoordinates = Parent.GetRealFromRelative(NegateY(_relativeCoordinates))
            End If

            Select Case dragAreaClicked
                Case 0
                    Coordinates = Parent.GetRelativeFromReal(New LineCoordinates(location - mouseOffset, location + (realCoordinates.EndPoint - realCoordinates.StartPoint) - mouseOffset))
                    If Not UseNegativeCoordinates Then Coordinates = NegateY(Coordinates)
                Case 1
                    If UseNegativeCoordinates Then
                        Coordinates = Parent.GetRelativeFromReal(New LineCoordinates(location - mouseOffset, realCoordinates.EndPoint))
                    Else
                        Coordinates = NegateY(Parent.GetRelativeFromReal(New LineCoordinates(location - mouseOffset, realCoordinates.EndPoint)))
                    End If
                Case 2
                    If UseNegativeCoordinates Then
                        Coordinates = Parent.GetRelativeFromReal(New LineCoordinates(realCoordinates.StartPoint, location - mouseOffset))
                    Else
                        Coordinates = NegateY(Parent.GetRelativeFromReal(New LineCoordinates(realCoordinates.StartPoint, location - mouseOffset)))
                    End If
                Case 3
                    If HasParallel Then
                        ParallelDistance = -((LinCalc(StartPoint.X, StartPoint.Y, EndPoint.X, EndPoint.Y, Parent.GetRelativeFromRealX(Mouse.GetPosition(Parent).X)) + (CInt(UseNegativeCoordinates) * 2 + 1) * Parent.GetRelativeFromRealY(Mouse.GetPosition(Parent).Y)))
                    End If
            End Select
            Coordinates = GetSnappedCoordinates()
            AutoRefresh = prevAutoRefresh
            RefreshVisual()
        End If

        MyBase.ParentMouseMove(e, location)
    End Sub
    Public Overrides Sub ParentMouseRightButtonDown(ByVal e As System.Windows.Input.MouseButtonEventArgs, ByVal location As System.Windows.Point)
        MyBase.ParentMouseRightButtonDown(e, location)
    End Sub
    Public Overrides Sub ParentMouseRightButtonUp(ByVal e As System.Windows.Input.MouseButtonEventArgs, ByVal location As System.Windows.Point)
        MyBase.ParentMouseRightButtonUp(e, location)
        If IsEditable Then
            Dim formatWin As New FormatTrendLineWindow(Me)
        End If
    End Sub

    Friend Overrides Sub Command_Executed(ByVal sender As Object, ByVal e As System.Windows.Input.ExecutedRoutedEventArgs)
        If e.Command Is ChartCommands.RemoveObject Then
            Parent.Children.Remove(Me)
        ElseIf e.Command Is ChartCommands.CreateLineParallel Then
            HasParallel = True
            If HasParent Then
                ParallelDistance = -((LinCalc(StartPoint.X, StartPoint.Y, EndPoint.X, EndPoint.Y, Parent.GetRelativeFromRealX(Mouse.GetPosition(Parent).X)) + (CInt(UseNegativeCoordinates) * 2 + 1) * Parent.GetRelativeFromRealY(Mouse.GetPosition(Parent).Y)))
            End If
            If Not AutoRefresh Then RefreshVisual()
        ElseIf e.Command Is ChartCommands.RemoveLineParallel Then
            HasParallel = False
            If Not AutoRefresh Then RefreshVisual()
        ElseIf e.Command Is ChartCommands.CreateObjectDuplicate Then
            Dim tl As New TrendLine
            Dim realPos As LineCoordinates = Parent.GetRealFromRelative(New LineCoordinates(StartPoint, EndPoint))
            realPos = New LineCoordinates(New Point(realPos.StartPoint.X, realPos.StartPoint.Y - 40), New Point(realPos.EndPoint.X, realPos.EndPoint.Y - 40))
            tl.UseNegativeCoordinates = UseNegativeCoordinates
            tl.Coordinates = Parent.GetRelativeFromReal(realPos)
            tl.Pen = Pen.Clone
            tl.ExtendLeft = ExtendLeft
            tl.ExtendRight = ExtendRight
            tl.AutoRefresh = AutoRefresh
            tl.IsSelectable = IsSelectable
            tl.IsEditable = IsEditable
            tl.RefreshVisual()
            Parent.Children.Add(tl)
        ElseIf e.Command Is ChartCommands.ToggleLineExtensionLeft Then
            ExtendLeft = Not ExtendLeft
        ElseIf e.Command Is ChartCommands.ToggleLineExtensionRight Then
            ExtendRight = Not ExtendRight
        Else
            MyBase.Command_Executed(sender, e)
        End If
    End Sub

    Public Overrides Function ContainsPoint(ByVal point As System.Windows.Point) As Boolean
        If HasParallel Then
            Return baseLineGeometry.FillContains(point, ClickLeniency, ToleranceType.Absolute) OrElse parallelLineGeometry.FillContains(point, ClickLeniency, ToleranceType.Absolute)
        Else
            Return baseLineGeometry.FillContains(point, ClickLeniency, ToleranceType.Absolute)
        End If
    End Function

    Public Sub Deserialize(ByVal serializedString As String) Implements ISerializable.Deserialize
        Try
            Dim v() As String = Split(serializedString, "/")
            Pen = PenFromString(v(0))
            StartPoint = (New PointConverter).ConvertFromString(v(1))
            EndPoint = (New PointConverter).ConvertFromString(v(2))
            ExtendLeft = v(3)
            ExtendRight = v(4)
            UseNegativeCoordinates = v(5)
            AutoRefresh = v(6)
        Catch ex As Exception
            Throw New ArgumentException("'TrendLine.Deserialize' got an error parsing the string '" & serializedString & "'.")
        End Try
    End Sub
    Public Function Serialize() As String Implements ISerializable.Serialize
        Return Join({
                    PenToString(Pen),
                    (New PointConverter).ConvertToString(StartPoint),
                    (New PointConverter).ConvertToString(EndPoint),
                    ExtendLeft,
                    ExtendRight,
                    UseNegativeCoordinates,
                    AutoRefresh
                }, "/")
    End Function
End Class


